import numpy as np
import pickle
import redis
import math
import matplotlib.pyplot as plt
from pprint import pprint

from redis import StrictRedis
import random
import pandas as pd

from user_cf import get_split_ratings, get_dict,\
    get_movie_user_dict

file_name = "./movielens/ratings.dat"

## 连接redis
pool = redis.ConnectionPool(host='127.0.0.1', db=2)
redis = redis.StrictRedis(connection_pool=pool, decode_responses=True)



## calculate the similarity of movies
def movie_similarity(reverse_dict:dict, train_dict:dict, i, j)->float:
    i_u = reverse_dict[i]
    j_u = reverse_dict[j]

    common = i_u & j_u

    if len(common)==0:
        return 0

    sum = 0
    bottom_0 = 0
    bottom_1 = 0
    for user in common:
        sum += train_dict[user][i] * train_dict[user][j]
        bottom_0 += pow(train_dict[user][i], 2)
        bottom_1 += pow(train_dict[user][j], 2)

    if sum == 0 or bottom_0 == 0 or bottom_1 == 0:
        return 0

    return sum/math.sqrt(bottom_0 * bottom_1)

## movie - movie similarity dict
## using brute way
## "movie_simi_dict" in redis
def get_movie_simi_dict(reverse_dict:dict, train_dict:dict)->dict:
    # 先看看redis里面有没有
    if redis.exists("movie_simi_dict"):
        return pickle.loads(redis.get("movie_simi_dict"))

    simi_dict = {}
    n_movie = 3952
    for movie_i in range(1, n_movie+1):
        if movie_i not in reverse_dict:
            continue
        simi_dict[movie_i] = {}
        for movie_j in range(1, n_movie+1):
            if movie_i == movie_j or (movie_j not in reverse_dict):
                continue
            elif movie_i < movie_j:
                simi = movie_similarity(reverse_dict, train_dict, movie_i, movie_j)
                if simi > 0:
                    simi_dict[movie_i][movie_j] = simi
            else:
                if movie_i in simi_dict[movie_j]:
                    simi_dict[movie_i][movie_j] = simi_dict[movie_j][movie_i]

    # 存入redis里面
    redis.set("movie_simi_dict", pickle.dumps(simi_dict))
    return simi_dict


## movie - movie similarity dict
## "movie_simi_dict_0" in redis
## "norm_movie_simi_dict_0" in redis
def get_movie_simi_dict_0(train_dict:dict, reverse_dict:dict):
    '''
    :param train_dict:  user-movie dict {user:{movie:score}}
    :param reverse_dict:  movie-user dict {movie:set(user)}
    :return: (unnormlized dict, normalized dict)
    '''

    # 先看看redis里面有没有
    if redis.exists("movie_simi_dict_0") and redis.exists("norm_movie_simi_dict_0"):
        return pickle.loads(redis.get("movie_simi_dict_0")), pickle.loads(redis.get("norm_movie_simi_dict_0"))

    movie_simi_dict_0 = {}
    for user, movies in train_dict.items():
        for i in movies.keys():
            if i not in movie_simi_dict_0:
                movie_simi_dict_0[i] = {}
            for j in movies.keys():
                if i==j:
                    continue
                if j not in movie_simi_dict_0[i]:
                    movie_simi_dict_0[i][j] = 0
                movie_simi_dict_0[i][j] += (1/math.log(1+len(movies)))


    max_val = -1
    for i, simi_movies in movie_simi_dict_0.items():
        for j in simi_movies.keys():
            movie_simi_dict_0[i][j] /= math.sqrt(len(reverse_dict[i])*len(reverse_dict[j]))
            if movie_simi_dict_0[i][j] > max_val:
                max_val = movie_simi_dict_0[i][j]
    redis.set('movie_simi_dict_0', pickle.dumps(movie_simi_dict_0))  # not normalized

    # normalization
    norm_movie_simi_dict_0 = {}
    for i, simi_movies in movie_simi_dict_0.items():
        norm_movie_simi_dict_0[i] = {}
        for j, score in simi_movies.items():
            norm_movie_simi_dict_0[i][j] = score/max_val
    redis.set('norm_movie_simi_dict_0', pickle.dumps(movie_simi_dict_0))  # normalized

    return movie_simi_dict_0, norm_movie_simi_dict_0

## 推荐
def recommend(movie_simi_dict:dict,train_dict:dict,user:int,k:int,n:int=5)->dict:
    rank = {}
    seen_movies = train_dict[user]
    # each movie seen
    for seen_movie, score in seen_movies.items():
        # each similar movie
        for simi_movie, simi in sorted(movie_simi_dict[seen_movie].items(),
                                       key=lambda x:x[1], reverse=True)[:k]:
            if simi_movie in seen_movies:
                continue
            if simi_movie not in rank:
                rank[simi_movie] = 0
            rank[simi_movie] += simi * score
    return rank

# recall, precision, coverage, popularity
def evaluate(simi_dict:dict, reverse_dict:dict, train_dict:dict, test_dict:dict, k:int=20):
    hit = 0
    all_reality = 0
    all_prediction = 0
    pop = 0

    recommended = set()
    all_movies = set()

    for u, reality in test_dict.items():
        all_reality += len(reality)  # calculate recall

        prediction = recommend(simi_dict, train_dict, u, k)

        all_prediction += len(prediction)  # calculate precision

        for i in prediction.keys():
            recommended.add(i)  # calculate coverage
            pop += math.log(1 + len(reverse_dict[i]))  # calculate popularity

            if i in reality:
                hit += 1

    for u in test_dict.keys():
        for i in train_dict[u].keys():
            all_movies.add(i)

    # print(hit)
    # print(all_reality)
    return hit/all_reality, hit/all_prediction, len(recommended)/len(all_movies), pop/all_prediction


if __name__ == "__main__":
    user = 1
    k = 20  # 选择最近的k个电影
    n = 10  # 推荐n部电影, test里面平均有4-5部电影

    train_record, test_record = get_split_ratings(file_name)
    train_dict = get_dict(train_record, "train")
    test_dict = get_dict(test_record, "test")
    reverse_dict = get_movie_user_dict(train_record)


    # brute way
    # movie_simi_dict = get_movie_simi_dict(reverse_dict, train_dict)

    # smart way
    _, movie_simi_dict = get_movie_simi_dict_0(train_dict, reverse_dict)

    for k in range(30,35,10):
        a,b,c,d = evaluate(movie_simi_dict, reverse_dict, train_dict, test_dict, k)
        print('k:%d'%k)
        print("recall: %f, precision: %f, coverage: %f, popularity: %f" %(a,b,c,d))




